Imports System.Collections
Imports System.Collections.Generic
Imports System.Text
Imports System.IO
Imports System.Reflection

Namespace Core
	Friend MustInherit Class NdxFile
		Inherits ClusteredFile
		Implements IEnumerable
		Implements IHasEncoding
		Protected Shared mNdxHeaderWriter As QuickSerializer
		Protected Shared mNdxEntryWriter As QuickSerializer

		Friend mSortFieldsConstructor As ConstructorInfo
		Friend mSortFieldsReadBuffer As Byte()
		Friend mSortFieldsWriter As QuickSerializer
		Friend mSortFieldsDbfBufferReader As QuickSerializer
		Friend mSortFieldsCount As Integer
		Friend mNdxHeader As NdxHeader
		Protected mEncoding As Encoding

		Shared Sub New()
			'ignoremissingfields
			mNdxHeaderWriter = New QuickSerializer(GetType(NdxHeader), DbfVersion.DbfDotNet, Nothing, 0, False, True)
			'ignoremissingfields
			mNdxEntryWriter = New QuickSerializer(GetType(NdxEntry), DbfVersion.DbfDotNet, Nothing, 0, False, True)
		End Sub

		Public Sub New()
		End Sub

		Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
			Return InternalGetEnumerator()
		End Function

		Protected MustOverride Function InternalGetEnumerator() As IEnumerator

		#Region "IHasEncoding Members"

		Private ReadOnly Property IHasEncoding_Encoding() As Encoding Implements IHasEncoding.Encoding
			Get
				Return mEncoding
			End Get
		End Property

		#End Region

        Protected Friend Overrides Function InternalGetRecord(recordNo As Int32, returnNullIfNotInCache As Boolean) As Record
            Throw New Exception("Invalid call, InternalGetPage should be called on NdxFile rather than InternalGetRecord")
        End Function

        Friend Function InternalGetPage(parentPage As NdxPage, recordNo As Int32, returnNullIfNotInCache As Boolean) As NdxPage
            Dim record As NdxPage = DirectCast(MyBase.InternalGetRecord(recordNo, returnNullIfNotInCache), NdxPage)
            If record IsNot Nothing Then
                record.ParentPage = parentPage
            End If
            Return record
        End Function

        Public Overrides Function NewRecord() As Object
            Throw New Exception("Invalid call, NewPage should be called on NdxFile rather than NewRecord")
        End Function

        Public Function NewPage(parentPage As NdxPage) As NdxPage
            Dim record As NdxPage = DirectCast(MyBase.NewRecord(), NdxPage)
            record.ParentPage = parentPage
            Return record
        End Function

        Friend Sub Dump()
            System.Diagnostics.Debug.WriteLine("-- Dump index:" & Me.mOriginalFile & "------------------------")
            System.Diagnostics.Debug.WriteLine("Root: " & Me.mNdxHeader.StartingPageRecordNo)

            System.Diagnostics.Debug.WriteLine(Me.mRecordCount & " records")

            Dim page As NdxPage = RootPage
            Dim lastNode As NdxEntry = Nothing
            page.Dump("", Nothing, Nothing, True, lastNode)
            System.Diagnostics.Debug.WriteLine("-----------------------------------------------------------------------------------")
        End Sub

        Protected ReadOnly Property RootPage() As NdxPage
            Get
                Dim page As NdxPage = InternalGetPage(DirectCast(Nothing, NdxPage), CInt(Me.mNdxHeader.StartingPageRecordNo), False)
                Return page
            End Get
        End Property
    End Class

    Friend NotInheritable Class NdxFile(Of TRecord As {DbfRecord, New})
        Inherits NdxFile
        Private mSortFieldType As Type
        Friend mDbfFile As DbfFile(Of TRecord)
        Private mSortOrder As SortOrder
        Friend mDbfIndex As DbfIndex(Of TRecord)

        Shared Sub New()
        End Sub

        Public Sub New()
        End Sub

        Friend Overloads Shared Function [Get](filePath As String, dbfFile As DbfFile(Of TRecord), sortOrder As SortOrder) As NdxFile(Of TRecord)
            Dim file As NdxFile(Of TRecord) = ClusteredFile.[Get](Of NdxFile(Of TRecord))(filePath, OpenFileMode.OpenOrCreate, 123)
            If file.ReaderCount = 1 Then
                file.mDbfFile = dbfFile
                file.mSortOrder = sortOrder
                file.Initialize()
                ' perhaps check encoding and version match 
            Else
            End If
            Return file
        End Function

        Public Shared Function CreateGeneric(generic As Type, innerType As Type, ParamArray args As Object()) As Object
            Dim specificType As System.Type = generic.MakeGenericType(New System.Type() {innerType})
            Return Activator.CreateInstance(specificType, args)
        End Function

        Protected Overrides Function OnCreateNewRecord(isNew As Boolean, recordNo As Int32) As Record
            Return New NdxPage()
        End Function

        Protected Overrides Sub OnReadRecordBuffer(buffer As Byte(), record As Record)
            Dim ndxPage As NdxPage = DirectCast(record, NdxPage)
            ndxPage.OnReadRecord(buffer)
        End Sub

        Friend Overrides Function OnFillWriteBuffer(record As Record, buffer As [Byte]()) As Boolean
            Dim ndxPage As NdxPage = DirectCast(record, NdxPage)
            If ndxPage.mIsModified Then
                ndxPage.OnFillWriteBuffer(buffer)
                ndxPage.mIsModified = False
                Return True
            End If

            Return False
        End Function

#Region "IEnumerable Members"

        Private Iterator Function Enumerate(parentPage As NdxPage, pageNo As Int32) As IEnumerable(Of TRecord)
            Dim page As NdxPage = InternalGetPage(parentPage, pageNo, False)
            For i As Integer = 0 To page.EntriesCount - 1
                Dim entry As NdxEntry = page.GetEntry(i)

                If entry.LowerPageRecordNo <> Int32.MaxValue Then
                    For Each record As TRecord In Enumerate(page, entry.LowerPageRecordNo)
                        Yield record
                    Next
                Else
                    Dim result As TRecord = DirectCast(mDbfFile.InternalGetRecord(entry.DbfRecordNo, False), TRecord)
                    Yield result
                End If
            Next
            ' page.ReleaseLock();
        End Function

#End Region

        Public Shadows Iterator Function GetEnumerator() As IEnumerator(Of TRecord)
            For Each r As TRecord In Enumerate(Nothing, Me.mNdxHeader.StartingPageRecordNo)
                Yield r
            Next
        End Function

        Protected Overrides Function InternalGetEnumerator() As IEnumerator
            Return DirectCast(Me.GetEnumerator(), IEnumerator)
        End Function

        Friend Sub OnDbfRecordModified(recordNo As Int32, oldBuffer As Byte(), newBuffer As Byte())
            UpdateIndex(recordNo, oldBuffer, newBuffer)
        End Sub

        Private Sub UpdateIndex(recordNo As Int32, oldBuffer As Byte(), newBuffer As Byte())
            Dim oldNdxEntryFields As SortFields = Nothing
            Dim newNdxEntryFields As SortFields = Nothing

            If oldBuffer IsNot Nothing Then
                ' remove old key
                oldNdxEntryFields = DirectCast(mSortFieldsConstructor.Invoke(Nothing), SortFields)
                mSortFieldsDbfBufferReader.Read(Me, oldBuffer, oldNdxEntryFields)
            End If

            If newBuffer IsNot Nothing Then
                ' remove old key
                newNdxEntryFields = DirectCast(mSortFieldsConstructor.Invoke(Nothing), SortFields)
                mSortFieldsDbfBufferReader.Read(Me, newBuffer, newNdxEntryFields)
            End If

            If oldNdxEntryFields IsNot Nothing AndAlso newNdxEntryFields IsNot Nothing Then
                Dim result As Integer() = New Integer(Me.mSortFieldsCount - 1) {}
                Dim isEqual As Boolean = oldNdxEntryFields.ChainCompare(newNdxEntryFields, result)
                If isEqual Then
                    Return
                End If
            End If

            If oldBuffer IsNot Nothing Then
                ' we should delete the old index entry
                Dim page As NdxPage
                page = InternalGetPage(Nothing, Me.mNdxHeader.StartingPageRecordNo, False)
                Dim oldEntry As New NdxEntry() With {.DbfRecordNo = recordNo, .mNdxNativeLowerPageNo = 0, .Fields = oldNdxEntryFields}
#If DUMP_INSERTS Then
				System.Diagnostics.Trace.WriteLine("Removing entry " & oldEntry.Fields.ToString() & " #" & oldEntry.DbfRecordNo)
				System.Diagnostics.Trace.WriteLine("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
#End If
                page.LocateAndDelete(oldEntry)
            End If

            If newBuffer IsNot Nothing Then
                ' create new key
                Dim ndxEntryFields As SortFields = DirectCast(mSortFieldsConstructor.Invoke(Nothing), SortFields)
                mSortFieldsDbfBufferReader.Read(Me, newBuffer, ndxEntryFields)

                ' now we can try to insert this tuple in the index
                Dim page As NdxPage

                If Me.mRecordCount = 0 Then
                    page = DirectCast(NewPage(Nothing), NdxPage)
                    Me.mNdxHeader.StartingPageRecordNo = page.RecordNo
                Else
                    page = InternalGetPage(Nothing, Me.mNdxHeader.StartingPageRecordNo, False)
                End If
                System.Diagnostics.Debug.Assert(page.ParentPage Is Nothing, "page.mParentPage should be null.")

                Dim newEntry As New NdxEntry() With { _
                     .DbfRecordNo = recordNo, _
                     .mNdxNativeLowerPageNo = 0, _
                     .Fields = ndxEntryFields _
                }
                ' System.Diagnostics.Trace.WriteLine("Inserting " + recordNo);
#If DUMP_INSERTS Then
				System.Diagnostics.Trace.WriteLine("Inserting entry " & newEntry.Fields.ToString() & " #" & newEntry.DbfRecordNo)
				System.Diagnostics.Trace.WriteLine("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
#End If
                page.LocateAndInsert(newEntry)
            End If
        End Sub



        Private Sub GetNewSortField(dbfRecord As Object)
            Throw New NotImplementedException()
        End Sub

        Private Sub GetOldSortField(dbfRecord As Object)
            Throw New NotImplementedException()
        End Sub

        Public Overrides Sub Flush()
            If mFileAccess <> FileAccess.Read Then
                MyBase.WriteHeader()
                MyBase.Flush()
            End If
        End Sub

        Protected Overrides Sub OnWriteHeader(mStream As Stream)
            Dim headerBuff As [Byte]() = New Byte(mNdxHeaderWriter.RecordWidth - 1) {}
            mNdxHeaderWriter.Write(Me, headerBuff, mNdxHeader)
            mStream.Seek(0, 0)
            mStream.Write(headerBuff, 0, headerBuff.Length)
        End Sub


        Public Function CountChildren() As Integer
            Return RootPage.CountChildren(Integer.MaxValue)
        End Function

        Protected Overrides Sub OnInitialize(stream As Stream)
            mEncoding = mDbfFile.Encoding

            Dim types As List(Of Type) = New List(Of Type)()
            Dim sortOrderColumns As List(Of ColumnDefinition) = New List(Of ColumnDefinition)()
            Dim dbfColumns As List(Of ColumnDefinition) = New List(Of ColumnDefinition)()

            If mSortOrder Is Nothing Then
                mSortOrder = New SortOrder(Of TRecord)(False)
            End If

            Dim headerBuff As [Byte]() = New Byte(mNdxHeaderWriter.RecordWidth - 1) {}
            mNdxHeader = New NdxHeader()
            Dim read As Integer = stream.Read(headerBuff, 0, headerBuff.Length)

            If read > 0 Then
                mNdxHeaderWriter.Read(Me, headerBuff, mNdxHeader)
                If mNdxHeader.KeyString1.Length < 255 Then
                    mNdxHeader.KeyString2 = String.Empty
                End If

                Dim keyString As String = (mNdxHeader.KeyString1 & mNdxHeader.KeyString2).Trim()
                If keyString.Length > 0 Then
                    mSortOrder.Fields.Clear()
                    mSortOrder.AddField(keyString)
                End If
            End If
            mRecordCount = CType(stream.Length \ 512, Int32)
            mSortFieldsCount = mSortOrder.Fields.Count

            If mSortFieldsCount <= 0 Then
                Throw New Exception("An Index require at least one column")
            ElseIf mSortFieldsCount > 2 Then
                ' (this is not hard to make this bigger but I did not yet)
                Throw New Exception("An Index cannot be defined on more than 2 columns.")
            End If

            For i As Integer = 0 To mSortFieldsCount - 1
                Dim ic As IndexColumn = mSortOrder.Fields(i)
                Dim cd As ColumnDefinition = mDbfFile.GetColumnByName(ic.ColumnName)
                types.Add(cd.mFieldInfo.FieldType)

                Dim sortOrderColumn As ColumnDefinition = New ColumnDefinition("f" & (i + 1).ToString(), cd, ic.Ascending, ic.Width)
                sortOrderColumns.Add(sortOrderColumn)

                Dim dbfColumn As ColumnDefinition = New ColumnDefinition("f" & (i + 1).ToString(), cd, ic.Ascending, ic.Width)
                dbfColumn.ExtendedProperties("OffSet") = cd.ExtendedProperties("OffSet")
                dbfColumn.ExtendedProperties("ColumnWidth") = cd.ExtendedProperties("ColumnWidth")
                dbfColumn.ExtendedProperties("Decimals") = cd.ExtendedProperties("Decimals")

                dbfColumns.Add(dbfColumn)
            Next

            mSortFieldType = GetType(SortFields(Of )).MakeGenericType(types.ToArray())
            mSortFieldsConstructor = mSortFieldType.GetConstructor(New Type() {})

            mSortFieldsWriter = New QuickSerializer(mSortFieldType, mDbfFile.Version, sortOrderColumns, 0, False, True)
            mSortFieldsReadBuffer = New Byte(mSortFieldsWriter.RecordWidth - 1) {}

            'setOffset
            mSortFieldsDbfBufferReader = New QuickSerializer(mSortFieldType, mDbfFile.Version, dbfColumns, 0, True, False)

            If read = 0 Then
                mRecordWidth = 512
                mHeaderWidth = 512
                mNdxHeader.TotalNoOfPages = 0
                mNdxHeader.KeyString1 = mSortOrder.ToKeyString()
                mNdxHeader.KeyString2 = ""
                mNdxHeader.KeyType = 0
                mNdxHeader.SizeOfKeyRecord = CInt(mSortFieldsWriter.RecordWidth)
                mNdxHeader.NoOfKeysPerPage = CUShort((512 - 4) \ (mSortFieldsWriter.RecordWidth + 8))
                mNdxHeader.StartingPageRecordNo = Int32.MaxValue
                mNdxHeader.TotalNoOfPages = 0
                mNdxHeader.UniqueFlag = mSortOrder.IsUnique
                mRecordCount = 0
                If mDbfFile IsNot Nothing AndAlso mDbfFile.RecordCount > 0 Then
                    Dim newBuffer As [Byte]() = Nothing
                    For i As Int32 = 0 To mDbfFile.RecordCount - 1
                        Dim r As TRecord = mDbfFile.InternalGetRecord(i)
                        r.SaveChanges()
                        'readIfNeeded
                        newBuffer = r.RecordHolder.GetCurrentBuffer(True)
                        UpdateIndex(i, Nothing, newBuffer)
#If DUMP_INSERTS Then
						Dump()
						Dim count = CountChildren()
						System.Diagnostics.Debug.Assert(count Is i + 1, "CountChildren:" & count & " should be:" & Convert.ToString((i + 1)))
#End If
                    Next
                End If
            Else
                ' perhaps we should read from the header
                mRecordWidth = 512
                mHeaderWidth = 512
            End If
            mDbfFile.mIndexes.Add(Me)

        End Sub


        Friend Function GetRecordByRowIndex(p As Int32) As TRecord

            If p = mLastRecordByRowIndex Then
                Return mLastRecordByRowRecord
            Else
                mLastRecordByRowIndex = p
                Dim page As NdxPage = DirectCast(InternalGetPage(Nothing, Me.mNdxHeader.StartingPageRecordNo, False), NdxPage)
                System.Diagnostics.Debug.Assert(page.ParentPage Is Nothing, "page.mParentPage should be null.")
                mLastRecordByRowRecord = GetRecordSlow(page, p)
                Return mLastRecordByRowRecord
            End If
        End Function

        Private mLastRecordByRowIndex As Int32 = Int32.MaxValue
        Private mLastRecordByRowRecord As TRecord

        Private Function GetRecordSlow(page As NdxPage, ByRef p As Int32) As TRecord
            Dim result As TRecord
            For i As Integer = 0 To page.EntriesCount - 1
                Dim entry As NdxEntry = page.GetEntry(i)

                If entry.LowerPageRecordNo = Int32.MaxValue Then
                    If p = 0 Then
                        Return mDbfFile.InternalGetRecord(entry.DbfRecordNo)
                    End If
                    p -= 1
                Else
                    ' returnNullIfNotInCache 
                    Dim ndxPage2 As NdxPage = TryCast(InternalGetPage(page, entry.LowerPageRecordNo, False), NdxPage)
                    result = GetRecordSlow(ndxPage2, p)
                    If result IsNot Nothing Then
                        Return result
                    End If
                End If
            Next
            Return Nothing
        End Function

        Friend Function Find(Of T1 As IComparable(Of T1))(indexedValue1 As T1) As IEnumerable(Of TRecord)
            Dim fields As SortFields(Of T1) = New SortFields(Of T1)()
            fields.f1 = indexedValue1

            Dim entry As New NdxEntry()
            entry.Fields = fields

            Dim pageFrom As NdxPage = Nothing
            Dim entryNo As Integer
            RootPage.FindFirstEntry(entry, pageFrom, entryNo)
            Return Nothing
        End Function

    End Class
End Namespace
